Service Worker的应用

Service worker本质上充当Web应用程序、浏览器与网络(可用时)之间的代理服务器,这个API旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,它还提供入口以推送通知和访问后台同步API

描述

Service Worker本质上也是浏览器缓存资源用的,只不过他不仅仅是Cache,也是通过worker的方式来进一步优化,其基于h5web worker,所以不会阻碍当前js线程的执行,其最主要的工作原理,1是后台线程,是独立于当前网页线程,2是网络代理,在网页发起请求时代理拦截,来返回缓存的文件。简单来说Service Worker就是一个运行在后台的Worker线程,然后它会长期运行,充当一个服务,很适合那些不需要独立的资源数据或用户互动的功能,最常见用途就是拦截和处理网络请求,以下是一些细碎的描述:

  • 基于web worker(一个独立于JavaScript主线程的独立线程,在里面执行需要消耗大量资源的操作不会堵塞主线程)。
  • web worker的基础上增加了离线缓存的能力。
  • 本质上充当Web应用程序(服务器)与浏览器之间的代理服务器(可以拦截全站的请求,并作出相应的动作->由开发者指定的动作)。
  • 创建有效的离线体验(将一些不常更新的内容缓存在浏览器,提高访问体验)。
  • 由事件驱动的,具有生命周期。
  • 可以访问cacheindexDB
  • 支持推送。
  • 可以让开发者自己控制管理缓存的内容以及版本。

Service worker还有一些其他的使用场景,以及service worker的标准能够用来做更多使web平台接近原生应用的事情:

  • 后台数据同步。
  • 响应来自其它源的资源请求。
  • 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据。
  • 在客户端进行CoffeeScriptLESSCJS/AMD等模块编译和依赖管理(用于开发目的)。
  • 后台服务钩子。
  • 自定义模板用于特定URL模式。性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片。
  • 可以配合App ManifestService Worker来实现PWA的安装和离线等功能。
  • 后台同步,启动一个service worker即使没有用户访问特定站点,也可以更新缓存。
  • 响应推送,启动一个service worker向用户发送一条信息通知新的内容可用。
  • 对时间或日期作出响应。
  • 进入地理围栏(LBS的一种应用)。

示例

实现一个简单的Service worker应用示例,这个示例可以在断网的时候同样可以使用,相关的代码在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,在这里就是用原生的Service Worker写一个简单示例,直接写原生的Service Worker比较繁琐和复杂,所以可以借助一些库例如Workbox等,在使用Service Worker之前有一些注意事项:

  • Service worker运行在worker上,也就表明其不能访问DOM
  • 其设计为完全异步,同步API(如XHRlocalStorage)不能在service worker中使用。
  • 出于安全考量,Service workers只能由HTTPS承载,localhost本地调试可以使用http
  • Firefox浏览器的用户隐私模式,Service Worker不可用。
  • 其生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)。

首先使用Node启动一个基础的web服务器,可以使用anywhere这个包,当然使用其他服务器都是可以的,执行完命令后访问http://localhost:7890/即可。另外写完相关代码后建议重启一下服务,之前我就遇到了无法缓存的问题,包括disk cachememory cache,要重启服务才解决。还有要打开的链接为localhost,自动打开浏览器可能并不是localhost所以需要注意一下。如果要清理缓存的话,可以在浏览器控制台的Application项目中Storage点击Clear site data就能清理在网站中的所有缓存了。如果使用express或者koa等服务器环境,还可以尝试使用Service Worker来缓存数据请求,同样提供数据请求的path即可。

$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/

编写一个index.html文件和sw.js文件,以及引入相关的资源文件,目录结构如下,可以参考https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,当然直接clone下来运行一个静态文件服务器就可以直接使用了。

simple--service-worker ├── static │ ├── avatar.png │ └── cache.js ├── index.html └── sw.js

html中引入相关文件即可,主要是为了借助浏览器环境,而关注的位置是js

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Service Worker</title>
    <style type="text/css">
        .avatar{
            width: 50px;
            height: 50px;
            border-radius: 50px;
        }
    </style>
</head>
<body>
    <img class="avatar" src="./static/avatar.png">
    <script type="text/javascript">
        navigator.serviceWorker
            .register("sw.js")
            .then(() => {
                console.info("注册成功");
            })
            .catch(() => {
                console.error("注册失败");
            });
    </script>
    <script src="./static/cache.js"></script>
</body>
</html>

使用Service worker的第一步,就是告诉浏览器,需要注册一个Service worker脚本,在这里我们直接将其写到了index.html文件中了。默认情况下,Service worker只对根目录/生效,如果要改变生效范围可以在register时加入第二个参数{ scope: "/xxx"},也可以直接在注册的时候就指定路径/xxx/sw.js

navigator.serviceWorker
.register("sw.js")
.then(() => {
    console.info("注册成功")
}).catch(err => {
    console.error("注册失败")
})

一旦登记成功,接下来都是Service worker脚本的工作,下面的代码都是写在service worker脚本里面的,登记后,就会触发install事件,service worker脚本需要监听这个事件。首先定义这个cache的名字,相当于是标识这一个缓存对象的键值,之后的urlsToCache数组是即将要缓存的数据,只要给定了相关的path,连数据请求也是同样能够缓存的,而不仅仅是资源文件,当然这边必须是Get的请求下使用,这是Cache这个API决定的。之后便是进行install,关于event.waitUntil可以理解为new Promise的作用,是要等待serviceWorker运行起来才继续后边的代码,其接受的实际参数只能是一个Promise。在MDN的解释是因为oninstallonactivate完成前需要一些时间,service worker标准提供一个waitUntil方法,当oninstall或者onactivate触发时被调用,接受一个promise,在这个promise被成功resolve以前,功能性事件不会分发到service worker。之后便是从caches取出这个CACHE_NAMEkey标识的cache,之后使用cache.addAll将数组中的path告诉cache,在第一次打开的时候,Service worker会自动去请求相关的数据并且缓存起来,使用Service worker去请求的数据,在Chrome控制台的Network中会显示一个小小的齿轮图标,很好辨认。

const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];

this.addEventListener("install", event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => {
            console.log("[Service Worker]", urlsToCache);
            return cache.addAll(urlsToCache);
        })
    );
});

之后是activated阶段,如果是第一次加载sw,在安装后,会直接进入activated阶段,而如果sw进行更新,情况就会显得复杂一些,流程如下:首先老的swA,新的sw版本为B, B进入install阶段,而A还处于工作状态,所以B进入waiting阶段,只有等到Aterminated后,B才能正常替换A的工作。这个terminated的时机有如下几种方式,1、关闭浏览器一段时间。2、手动清除Service Worker3、在sw安装时直接跳过waiting阶段。然后就进入了activated阶段,激活sw工作,activated阶段可以做很多有意义的事情,比如更新存储在Cache中的keyvalue。在下边的代码中,实现了不在白名单的CACHE_NAME就清理,可以在这里实现一个version也就是版本的控制,之前的版本就要清理等,另外还查看了一下目前的相关缓存。

this.addEventListener("activate", event => {
    // 不在白名单的`CACHE_NAME`就清理
    const cacheWhitelist = ["service-worker-demo"];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    // 查看一下缓存
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
    );
});

之后便是拦截请求的阶段了,该阶段是sw关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作,所有的缓存部分,都是在该阶段。首先我们直接拦截掉所有的请求,在最前边的判断操作是为了防止所有的请求都被拦截从而都在worker里边发起请求,当然不进行判断也是可以使用的。然后对于请求如果匹配到了缓存,那么就直接从缓存中取得数据,否则就使用fetch去请求新的。另外如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存。

this.addEventListener("fetch", event => {
    const url = new URL(event.request.url);
    if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
        event.respondWith(
            caches.match(event.request).then(resp => {
                if (resp) {
                    console.log("fetch ", event.request.url, "有缓存,从缓存中取");
                    return resp;
                } else {
                    console.log("fetch ", event.request.url, "没有缓存,网络获取");
                    return fetch(event.request);
                    // // 如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存
                    // return fetch(event.request).then(response => {
                    //     return caches.open(CACHE_NAME).then(cache => {
                    //         cache.put(event.request, response.clone());
                    //         return response;
                    //     });
                    // });
                }
            })
        );
    }
});

第一次打开时控制台的输出:

cache.js loaded [Service Worker] (3) ['/', '/static/avatar.png', '/static/cache.js'] 注册成功 (3) [Request, Request, Request]

第二次及之后打开的控制台输出:

fetch http://localhost:7811/static/avatar.png 有缓存,从缓存中取 fetch http://localhost:7811/static/cache.js 有缓存,从缓存中取 注册成功 cache.js loaded

至此我们就完成了一个简单的示例,在第二次打开页面的时候,我们可以将浏览器的网络连接断开,例如关闭文件服务器或者在控制台的Network中选择Offline,而我们也可以看到页面依旧正常加载,不需要网络服务,另外也可以在Network的相关的数据的Size列会出现(ServiceWorker)这个信息,说明资源是从ServiceWorker加载的缓存数据。可以在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-workerclone下来后运行这个示例。

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Service Worker</title>
    <style type="text/css">
        .avatar{
            width: 50px;
            height: 50px;
            border-radius: 50px;
        }
    </style>
</head>
<body>
    <img class="avatar" src="./static/avatar.png">
    <script type="text/javascript">
        navigator.serviceWorker
            .register("sw.js")
            .then(() => {
                console.info("注册成功");
            })
            .catch(() => {
                console.error("注册失败");
            });
    </script>
    <script src="./static/cache.js"></script>
</body>
</html>
// sw.js
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];

this.addEventListener("install", event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => {
            console.log("[Service Worker]", urlsToCache);
            return cache.addAll(urlsToCache);
        })
    );
});

this.addEventListener("activate", event => {
    // 不在白名单的`CACHE_NAME`就清理
    const cacheWhitelist = ["service-worker-demo"];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    // 查看一下缓存
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
    );
});

this.addEventListener("fetch", event => {
    const url = new URL(event.request.url);
    if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
        event.respondWith(
            caches.match(event.request).then(resp => {
                if (resp) {
                    console.log("fetch ", event.request.url, "有缓存,从缓存中取");
                    return resp;
                } else {
                    console.log("fetch ", event.request.url, "没有缓存,网络获取");
                    return fetch(event.request);
                    // // 如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存
                    // return fetch(event.request).then(response => {
                    //     return caches.open(CACHE_NAME).then(cache => {
                    //         cache.put(event.request, response.clone());
                    //         return response;
                    //     });
                    // });
                }
            })
        );
    }
});
// cache.js
console.log("cache.js loaded");
// avatar.png
// [byte]png

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://github.com/mdn/sw-test/ https://zhuanlan.zhihu.com/p/25459319 https://zhuanlan.zhihu.com/p/115243059 https://zhuanlan.zhihu.com/p/161204142 https://github.com/youngwind/service-worker-demo https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g https://developer.mozilla.org/zh-CN/docs/Web/API/Cache https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md